package org.easymodelers.template;

import java.io.StringReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.easymodelers.expression.ExpressionData;
import org.easymodelers.expression.ExpressionParser;
import org.easymodelers.template.utils.ContextManager;
import org.easymodelers.template.utils.DynamicModelManager;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EFactory;
import org.eclipse.emf.ecore.EObject;

public class TemplateHandler {
	private NamespaceData currentNamespace; 
	private Map<String,NamespaceData> registeredNamespaces = new HashMap<String, NamespaceData>();
	private Map<String,Object> context = new HashMap<String, Object>();

	public void setContext(Map<String,Object> context) {
		this.context = context;
	}
	
	public void registerDefaultNamespaces() {
		registerNamespace(
				new NamespaceData("xmi", "http://www.omg.org/XMI", null, null, false));
	}
	
	public void registerNamespace(NamespaceData data) {
		registeredNamespaces.put(data.getName(), data);
	}
	
	public NamespaceData getRegisteredNamespace(String namespaceName) {
		NamespaceData namespace = registeredNamespaces.get(namespaceName);
		if (namespace==null) {
			String message = "Unable to find namespace with name "+namespaceName;
			throw new TemplateProcessingException(message);
		}
		
		return namespace;
	}
	
	private String preprocessTemplateContent(String content) {
		return content;
	}
	
	private String addNamespacesInContentHeader(String content) {
		int index = content.indexOf(">");
		String firstXmlElement = content.substring(0, index+1);
		
		StringBuffer newFirstXmlElement = new StringBuffer();
		newFirstXmlElement.append(firstXmlElement.substring(0, firstXmlElement.length()-1));
		newFirstXmlElement.append(" xmi:version=\"2.0\"");
		for (String namespaceName : registeredNamespaces.keySet()) {
			newFirstXmlElement.append(" ");
			NamespaceData namespace = getRegisteredNamespace(namespaceName);
			newFirstXmlElement.append("xmlns:");
			newFirstXmlElement.append(namespace.getName());
			newFirstXmlElement.append("=\"");
			newFirstXmlElement.append(namespace.getIdentifier());
			newFirstXmlElement.append("\"");
		}
		newFirstXmlElement.append(">");

		return content.replace(firstXmlElement, newFirstXmlElement.toString());
	}
	
	//http://dom4j.sourceforge.net/dom4j-1.6.1/guide.html
	private Document loadDocument(String content) {
		try {
			SAXReader reader = new SAXReader();
			String updatedContent = addNamespacesInContentHeader(content);
			Document document = reader.read(new StringReader(updatedContent));
			return document;
		} catch(Exception ex) {
			String message = "Unable to load the template because of errors during its parsing.";
			throw new TemplateProcessingException(message, ex);
		}
	}
	
	private boolean hasElement(EObject parentEObject,
						Element parentElement, Element element) {
		String typeName = element.attributeValue("type");
		if (typeName==null || "".equals(typeName.trim())) {
			String message = "The type for element "+element.getName()+" is null or empty.";
			throw new TemplateProcessingException(message);
		}
		if (typeName.startsWith("${") && typeName.endsWith("}")) {
			String message = "The type for element "+element.getName()+" is an expression.";
			throw new TemplateProcessingException(message);
		}

		int index = typeName.indexOf(":");
		NamespaceData namespace = getRegisteredNamespace(typeName.substring(0, index));
		typeName = typeName.replace(namespace.getName()+":", "");

		String attributeName = element.getName();

		//System.out.println("parentEObject = "+parentEObject);

		Map<String, String> attributes = new HashMap<String, String>();
		for (Iterator i = element.attributeIterator(); i.hasNext();) {
			Attribute attr = (Attribute) i.next();
			if (!"type".equals(attr.getName())) {
				attributes.put(attr.getName(), attr.getValue());
			}
		}

		if (DynamicModelManager.isEListAttribute(parentEObject, attributeName)) {
			EList listAttribute = DynamicModelManager.getEListAttribute(
											parentEObject, attributeName);
			int nodeCount = 0;
			for (Iterator i = parentElement.elementIterator(); i.hasNext();) {
				Element elt = (Element) i.next();
				if (element.getName().equals(elt.getName())) {
					nodeCount++;
				}
			}
			//if (listAttribute.size()==nodeCount) {

				return DynamicModelManager.hasEObjectInList(listAttribute, attributes, context);
			/*} else if (listAttribute.size()<nodeCount) {
				return false;
			}*/
		} else {
			EObject o = (EObject) DynamicModelManager.getAttribute(parentEObject, attributeName);
			if (o!=null) {
				return DynamicModelManager.checkEObject(o, attributes, context);
			} else {
				return false;
			}
		}
	}
	
	private EObject createEObjectByType(Element element) {
		return createEObject(element, element.attributeValue("type"));
	}

	private EObject createEObjectByName(Element element) {
		return createEObject(element, element.getNamespacePrefix()+":"+element.getName());
	}

	private EObject createEObject(Element element, String typeName) {
		System.out.println("Creating object of type = "+typeName+" for element with name "+element.getName()+"...");
		int index = typeName.indexOf(":");
		NamespaceData namespace = getRegisteredNamespace(typeName.substring(0, index));
		typeName = typeName.replace(namespace.getName()+":", "");
		return DynamicModelManager.createEObject(namespace.getFactory(), typeName);
	}
	
	private boolean isExpressionValue(String value) {
		return (value.indexOf("${")!=-1);
	}

	private void findAndSetEObject(EObject rootEObject, EObject currentEObject,
			final String name, String expression, Set<ElementPostProcessor> postProcessors) {
		findAndSetEObject(rootEObject, currentEObject, name, expression, false, postProcessors);
	}
		
	private void findAndSetEObject(EObject rootEObject, final EObject currentEObject,
						final String name, String expression, boolean lateBinding, Set<ElementPostProcessor> postProcessors) {
		System.out.println("Finding object for expression "+expression+"...");
		ExpressionParser parser = new ExpressionParser(expression);
		final ExpressionData expressionData = parser.parse();
		if (expressionData.getFunction()!=null) {
			if ("children".equals(expressionData.getFunction())) {
				EObject ref = DynamicModelManager.findEObjectByClass(rootEObject, expressionData.getType(),
						expressionData.getConditionFieldName(), expressionData.getConditionFieldValue());
				DynamicModelManager.setEObject(currentEObject, name, ref);
			}
		} else {
			if (lateBinding) {
				postProcessors.add(new ElementPostProcessor() {
					public void postprocessElement() {
						NamespaceData namespaceData = getRegisteredNamespace(expressionData.getNamespace());
						EObject ref = null;
						if (namespaceData.isNameBasedComparison()) {
							ref = DynamicModelManager.findEObjectByName(namespaceData.getRootEObject(), expressionData.getType(),
											expressionData.getConditionFieldName(), expressionData.getConditionFieldValue());
						} else {
							ref = DynamicModelManager.findEObjectByClass(namespaceData.getRootEObject(), expressionData.getType(),
									expressionData.getConditionFieldName(), expressionData.getConditionFieldValue());
						}
						DynamicModelManager.setEObject(currentEObject, name, ref);
					}
				});
			} else {
				if (expressionData.getNamespace()==null) {
					Object value = ContextManager.getValueFromContext(context,
												expressionData.getContextValue());
					DynamicModelManager.setAttribute(currentEObject, name, value);
					return;
				} else if ("enum".equals(expressionData.getNamespace())) {
					Object value = DynamicModelManager.getEnumValue(
							expressionData.getType(), expressionData.getFieldName(),
							currentNamespace.getEnumPackages());
					DynamicModelManager.setAttribute(currentEObject, name, value);
					return;
				}

				NamespaceData namespaceData = getRegisteredNamespace(expressionData.getNamespace());
				EObject ref = null;
				String conditionFieldValue = expressionData.getConditionFieldValue();
				if (conditionFieldValue==null) {
					String conditionFieldContextValue = expressionData.getConditionFieldContextValue();
					if (conditionFieldContextValue!=null) {
						conditionFieldValue = (String) ContextManager.getValueFromContext(context, conditionFieldContextValue);
					}
				}

				if (namespaceData.isNameBasedComparison()) {
					ref = DynamicModelManager.findEObjectByName(namespaceData.getRootEObject(), expressionData.getType(),
									expressionData.getConditionFieldName(), conditionFieldValue);
				} else {
					ref = DynamicModelManager.findEObjectByClass(namespaceData.getRootEObject(), expressionData.getType(),
							expressionData.getConditionFieldName(), conditionFieldValue);
				}
				//System.out.println("ref = "+ref);
				//System.out.println("currentEObject = "+currentEObject);
				DynamicModelManager.setEObject(currentEObject, name, ref);
			}
		}
	}
	
	private void fillEObjectAttributes(EObject rootEObject, EObject o, Element element, Set<ElementPostProcessor> postProcessors) {
		System.out.println("Filling attributes for object "+o.getClass().getName()+" ("+element.getName()+")");
		String lateBindingValue = element.attributeValue("late-binding");
		boolean lateBinding = (element.attributeValue("late-binding")!=null && lateBindingValue.equals("true"));
		Map<String, String> attributes = new HashMap<String, String>();
		for (Iterator i = element.attributes().iterator(); i.hasNext(); ) {
			Attribute attribute = (Attribute) i.next();
			if ("type".equals(attribute.getName())) {
				continue;
			}
			if ("late-binding".equals(attribute.getName())) {
				continue;
			}

			System.out.println(" -> Handling attribute with name "+attribute.getName());
			if (!isExpressionValue(attribute.getValue())) {
				DynamicModelManager.setAttribute(o, attribute.getName(), attribute.getValue());
			} else {
				findAndSetEObject(rootEObject, o, attribute.getName(), attribute.getValue(), lateBinding, postProcessors);
			}
		}
	}
	
	private void handleElement(EFactory factory, EObject rootEObject, EObject parentEObject,
						Element parentElement, Set<ElementPostProcessor> postProcessors) {
		System.out.println("Handling children elements of element "+parentElement.getName()+"...");
		for (Iterator i = parentElement.elementIterator(); i.hasNext();) {
            Element element = (Element) i.next();
    		System.out.println("Handling element "+element.getName()+"...");
            EObject o = null;
            if (!hasElement(parentEObject, parentElement, element)) {
            	if (isReference(element)) {
            		o = findEObject(element);
                	System.out.println(" -> This element is a reference of type "+o.getClass().getName());
            	} else {
            		o = createEObjectByType(element);
                	System.out.println(" -> This element has been created - type: "+o.getClass().getName());
            		fillEObjectAttributes(rootEObject, o, element, postProcessors);
            	}
            	
            	if (o!=null) {
            		DynamicModelManager.setEObject(parentEObject, element.getName(), o);
            	}
            } else {
        		o = findExistingEObject(element, parentEObject, parentElement);
            }
            
            handleElement(factory, rootEObject, o, element, postProcessors);
            
        }
		System.out.println("Children elements of element "+parentElement.getName()+" handled");
	}
	
	private boolean isReference(Element element) {
		return (element.attribute("href")!=null);
	}

	private EObject findExistingEObject(Element element, EObject parentEObject, Element parentElement) {
		String typeName = element.attributeValue("type");
		if (typeName==null || "".equals(typeName.trim())) {
			String message = "The type for element "+element.getName()+" is null or empty.";
			throw new TemplateProcessingException(message);
		}
		if (typeName.startsWith("${") && typeName.startsWith("}")) {
			String message = "The type for element "+element.getName()+" is an expression.";
			throw new TemplateProcessingException(message);
		}

		int index = typeName.indexOf(":");
		NamespaceData namespace = getRegisteredNamespace(typeName.substring(0, index));
		typeName = typeName.replace(namespace.getName()+":", "");

		String attributeName = element.getName();

		Map<String, String> attributes = new HashMap<String, String>();
		for (Iterator i = element.attributeIterator(); i.hasNext();) {
			Attribute attr = (Attribute) i.next();
			if (!"type".equals(attr.getName())) {
				attributes.put(attr.getName(), attr.getValue());
			}
		}
		
		if (DynamicModelManager.isEListAttribute(parentEObject, attributeName)) {
			EList listAttribute = DynamicModelManager.getEListAttribute(
											parentEObject, attributeName);
			int nodeCount = 0;
			for (Iterator i = parentElement.elementIterator(); i.hasNext();) {
				Element elt = (Element) i.next();
				if (element.getName().equals(elt.getName())) {
					nodeCount++;
				}
			}

			for (Object o : listAttribute) {
				//System.out.println("checkEObject - "+o);
				if (DynamicModelManager.checkEObject((EObject)o, attributes, context)) {
					return (EObject) o;
				}
			}
		} else {
			EObject o = (EObject) DynamicModelManager.getAttribute(parentEObject, attributeName);
			if (o!=null && DynamicModelManager.checkEObject(o, attributes, context)) {
				return o;
			}
		}

		return null;
	}

	private EObject findEObject(Element element) {
		String href = element.attributeValue("href");
		ExpressionData expression = new ExpressionParser(href).parse();
		System.out.println("Trying to find object for expression "+href);
		
		NamespaceData namespace = getRegisteredNamespace(expression.getNamespace());
		EObject rootEObjectForNamespace = namespace.getRootEObject();
		if ("/".equals(expression.getType())) {
			return rootEObjectForNamespace;
		}

		EObject o = null;
		if (namespace.isNameBasedComparison()) {
			if (DynamicModelManager.checkEObject(rootEObjectForNamespace, expression.getType(), null, null, true)) {
				return rootEObjectForNamespace;
			}
			o = DynamicModelManager.findEObjectByName(rootEObjectForNamespace, expression.getType(),
													expression.getConditionFieldName(),
													expression.getConditionFieldValue());
		} else {
			if (DynamicModelManager.checkEObject(rootEObjectForNamespace, expression.getType(), null, null, false)) {
				return rootEObjectForNamespace;
			}
			o = DynamicModelManager.findEObjectByClass(rootEObjectForNamespace, expression.getType(),
													expression.getConditionFieldName(),
													expression.getConditionFieldValue());
		}
		if (o==null) {
			return null;
		}

		String fieldName = expression.getFieldName();
		if (fieldName!=null) {
			if (namespace.isNameBasedComparison()) {
				return DynamicModelManager.getEClassAttribute(o, fieldName);
			} else {
				return (EObject) DynamicModelManager.getAttribute(o, fieldName);
			}
		} else {
			return o;
		}
	}

	public EObject processTemplate(String content) throws Exception {
		//load template
		String processedContent = preprocessTemplateContent(content);
		Document document = loadDocument(processedContent);
		Element rootElement = document.getRootElement();
		
		//initialize root entities
		String namespacePrefix = rootElement.getNamespacePrefix();
		NamespaceData namespace = getRegisteredNamespace(namespacePrefix);
		this.currentNamespace = namespace;
		
		EFactory factory = namespace.getFactory();
		EObject rootEObject = null;
		if (namespace.getRootEObject()!=null) {
			rootEObject = namespace.getRootEObject();
		} else {
			rootEObject = createEObjectByName(rootElement);
			namespace.setRootEObject(rootEObject);
		}

		//handle template
		Set<ElementPostProcessor> postProcessors = new HashSet<ElementPostProcessor>();
		handleElement(factory, rootEObject, rootEObject, rootElement, postProcessors);
		
        //postprocessing
        for (ElementPostProcessor postProcessor : postProcessors) {
			postProcessor.postprocessElement();
		}

        return rootEObject;
	}

}
